/****************************************************************************
**
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
**     the names of its contributors may be used to endorse or promote
**     products derived from this software without specific prior written
**     permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtCore/qendian.h>
#include <QVector>
#include <QDebug>
#include <QFile>
#include <QTextStream>

#include "wavfile.h"

/*!
 * \class WavFile
 * Helper class for reading and writing to a WAV file.
 * Reads header from WAV file and extracts sound format from header.
 * Sound data can be read separately.
 * Header and sound data  can be also write to WAV file.
 *
 * See https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
 */

struct chunk
{
    char        id[4];
    quint32     size;
};

struct RIFFHeader
{
    chunk       descriptor;     // "RIFF"
    char        type[4];        // "WAVE"
};

struct WAVEHeader
{
    chunk       descriptor;
    quint16     audioFormat;
    quint16     numChannels;
    quint32     sampleRate;
    quint32     byteRate;
    quint16     blockAlign;
    quint16     bitsPerSample;
};

struct DATAHeader
{
    chunk       descriptor;
};

struct CombinedHeader
{
    RIFFHeader  riff;
    WAVEHeader  wave;
    DATAHeader  data;
};

static const int HeaderLength = sizeof(CombinedHeader);

/*!
    Constructs WavFile
*/
WavFile::WavFile(const QAudioFormat &format, qint64 dataLength)
    : audioFormat(format)
    , dataBufferLength(dataLength)
{

}

/*!
    Reads header from the file given at the \a device parameter.
    Returns true if the reading of the header was successfull.
*/
bool WavFile::readHeader(QIODevice &device)
{
    bool result = true;

    if (!device.isSequential())
        result = device.seek(0);
    // else, assume that current position is the start of the header

    if (result) {
        CombinedHeader header;
        result = (device.read(reinterpret_cast<char *>(&header), HeaderLength) == HeaderLength);
        if (result) {
            if ((memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0
                || memcmp(&header.riff.descriptor.id, "RIFX", 4) == 0)
                && memcmp(&header.riff.type, "WAVE", 4) == 0
                && memcmp(&header.wave.descriptor.id, "fmt ", 4) == 0
                && header.wave.audioFormat == 1 // PCM
            ) {
                if (memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0)
                    audioFormat.setByteOrder(QAudioFormat::LittleEndian);
                else
                    audioFormat.setByteOrder(QAudioFormat::BigEndian);

                audioFormat.setChannels(qFromLittleEndian<quint16>(header.wave.numChannels));
                audioFormat.setCodec("audio/pcm");
                audioFormat.setFrequency(qFromLittleEndian<quint32>(header.wave.sampleRate));
                audioFormat.setSampleSize(qFromLittleEndian<quint16>(header.wave.bitsPerSample));

                switch (header.wave.bitsPerSample) {
                case 8:
                    audioFormat.setSampleType(QAudioFormat::UnSignedInt);
                    break;
                case 16:
                    audioFormat.setSampleType(QAudioFormat::SignedInt);
                    break;
                default:
                    result = false;
                    qWarning("Unknown sample type at WAV file header.");
                }

                dataBufferLength = device.size() - HeaderLength;
            } else {
                result = false;
                qWarning("Error at WAV file header.");
            }
        }
    }

    return result;
}

/*!
    Writes WAV file header to the \a device given at the parameter.
*/
bool WavFile::writeHeader(QIODevice &device)
{
    CombinedHeader header;

    memset(&header, 0, HeaderLength);

    // RIFF header
    if (audioFormat.byteOrder() == QAudioFormat::LittleEndian)
        strncpy(&header.riff.descriptor.id[0], "RIFF", 4);
    else
        strncpy(&header.riff.descriptor.id[0], "RIFX", 4);
    qToLittleEndian<quint32>(quint32(dataBufferLength + HeaderLength - 8),
                             reinterpret_cast<unsigned char*>(&header.riff.descriptor.size));
    strncpy(&header.riff.type[0], "WAVE", 4);

    // WAVE header
    strncpy(&header.wave.descriptor.id[0], "fmt ", 4);
    qToLittleEndian<quint32>(quint32(16),
                             reinterpret_cast<unsigned char*>(&header.wave.descriptor.size));
    qToLittleEndian<quint16>(quint16(1),
                             reinterpret_cast<unsigned char*>(&header.wave.audioFormat));
    qToLittleEndian<quint16>(quint16(audioFormat.channels()),
                             reinterpret_cast<unsigned char*>(&header.wave.numChannels));
    qToLittleEndian<quint32>(quint32(audioFormat.frequency()),
                             reinterpret_cast<unsigned char*>(&header.wave.sampleRate));
    qToLittleEndian<quint32>(quint32(audioFormat.frequency() * audioFormat.channels() * audioFormat.sampleSize() / 8),
                             reinterpret_cast<unsigned char*>(&header.wave.byteRate));
    qToLittleEndian<quint16>(quint16(audioFormat.channels() * audioFormat.sampleSize() / 8),
                             reinterpret_cast<unsigned char*>(&header.wave.blockAlign));
    qToLittleEndian<quint16>(quint16(audioFormat.sampleSize()),
                             reinterpret_cast<unsigned char*>(&header.wave.bitsPerSample));

    // DATA header
    strncpy(&header.data.descriptor.id[0], "data", 4);
    qToLittleEndian<quint32>(quint32(dataBufferLength),
                             reinterpret_cast<unsigned char*>(&header.data.descriptor.size));

    return (device.write(reinterpret_cast<const char *>(&header), HeaderLength) == HeaderLength);
}

/*!
    Gets the audio format of the WAV file.
*/
const QAudioFormat& WavFile::format() const
{
    return audioFormat;
}

/*!
    Gets the length of sound data from the WAV file.
*/
qint64 WavFile::dataLength() const
{
    return dataBufferLength;
}

/*!
    Gets the length of header from the WAV file.
*/
qint64 WavFile::headerLength()
{
    return HeaderLength;
}

/*!
    Writes the length of the sound data to WAV file.
*/
bool WavFile::writeDataLength(QIODevice &device, qint64 dataLength)
{
    bool result = false;
    if (!device.isSequential()) {
        device.seek(40);
        unsigned char dataLengthLE[4];
        qToLittleEndian<quint32>(quint32(dataLength), dataLengthLE);
        result = (device.write(reinterpret_cast<const char *>(dataLengthLE), 4) == 4);
    }
    return result;
}

/*!
    Reads sound data from the \a device to the given \a buffer by using \a outputFormat.
*/
qint64 WavFile::readData(QIODevice &device, QByteArray &buffer,
                         QAudioFormat outputFormat)
{
    if (QAudioFormat() == outputFormat)
        outputFormat = audioFormat;

    qint64 result = 0;

    QVector<char> inputSample(2 * audioFormat.channels());

    qint16 *output = reinterpret_cast<qint16*>(buffer.data());

    while (result < buffer.size()) {
        if (device.read(inputSample.data(), inputSample.count())) {
            int inputIdx = 0;
            for (int outputIdx = 0; outputIdx < outputFormat.channels(); ++outputIdx) {
                const qint16* input = reinterpret_cast<const qint16*>(inputSample.data() + 2 * inputIdx);
                *output++ = qFromLittleEndian<qint16>(*input);
                result += 2;
                if (inputIdx < audioFormat.channels())
                    ++inputIdx;
            }
        } else {
            break;
        }
    }
    return result;
}
